前言

在当前Android性能不断提高的情况下,Java语言的一些缺陷逐渐被掩盖起来。什么时候使用NDK呢?

当设计的算法要利用Dalvik虚拟机中的所有处理器资源,而且原生运行较为有利,或者你希望的操作拥有尽可能快的处理速度时,就需要使用NDK。

另一个原因则是方便移植,特别对于OpenGL ES应用程序。

再一个原因就是为安全考虑。

但是Java与原生C语言之间的转换也会增加一些资源开销。

开源C库

  • ffmpeg 音视频解码
  • opencore 音视频
  • opencv 图像处理

准备工作

Google官网ndk地址
直接下载ndk r10e最新版

以MAC OS系统为为例,打开命令行窗口

1.获取ndk存放目录的权限。

chmod a+x android-ndk-r10c-darwin-x86_64.bin

2.解压缩

./android-ndk-r10c-darwin-x86_64.bin  
(或直接把ndk文件拖进命令行)

3.配置变量环境

pico .bash_profile

然后进行编辑

export NDK_ROOT=/Volumes/macwork/develop/ndk_r10e
export PATH=${PATH}:$NDK_ROOT

ndk环境变量配置截图

4.更新环境变量配置

source .bash_profile

然后再进入samples/hello-jni目录,再执行ndk-build目录就可以编译了

5.进入Android studio,由于项目是用Eclipse写的,请选择Import Project导入Hello-jni项目,然后File -> Project Structure配置ndk

as ndk配置

若出现NDK integration is deprecated in the current plugin问题,在 gradle.properties 中配置 android.useDeprecatedNdk=true 即可。as2.0已支持ctrl + 点击跳转到本地方法的定义c函数,没有定义过的方法也会高亮显示,还能自动导包。还在用eclipse的童鞋,you are drunk!都什么年代了。然后可以运行了。

开动学习啦

1.build.gradle配置,mk文件配置

先看app目录下面的build.gradle文件,需要配置jni模块名:

1
2
3
ndk {
moduleName "hello-jni"
}

实际还有更多可以配置

1
2
3
4
5
6
ndk {
moduleName "hello-jni" //模块名
cFlags "-DANDROID_NDK -D_DEBUG DNULL=0" // 定义一些宏指令(macros)
ldLibs "EGL", "GLESv3", "dl", "log" // 在这里添加你原先在makefile里ldlibs所链接的库
stl "stlport_shared" // 使用 shared stlport library
}

2.增加一个sayHello()方法

1.在hello-jni.c 中增加如下代码

1
2
3
jstring Java_com_example_hellojni_HelloJni_sayHello(JNIEnv *env,jobject thiz){
return (*env)->NewStringUTF(env,"hello world");
}

应该注意开头的J字要大写,随后的为包名类名本地方法名

2.然后在HelloJni.java中写上该方法

1
public native String sayHello();

然后就可以调用了。其中使用了闭包来导入了编译好的.so文件

1
2
3
static {
System.loadLibrary("hello-jni");
}

3.或可以使用javah命令直接生成native c头文件
进入到hello-jni/app/src/main/java 目录,执行

1
javah -d ../jni com.example.hellojni.HelloJni

即可生成头文件。

4.运行时as2.0需要关闭instant-run的功能,否则会出现.so文件找不到的错误。c文件的编译也不需要我们费心了,as自动会编译好。

3.C调用Java代码

C调用logcat打印日志

  • 1.修改build.gradle里ndk节点的内容:
1
2
3
4
5
ndk {
moduleName "HelloJNI"
stl "stlport_static" //单步调试支持
ldLibs "log" //添加c调用logcat打印日志的支持库
}

然后添加以下代码到buildTypes下:

1
2
3
debug {
jniDebuggable true
}
  • 2.引入头文件 #include<android/log.h>,其中包含__android_log_print()方法,为方便使用做个宏定义:
1
2
3
4
5
6
#include <android/log.h>
#define LOG_TAG "clog"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__);
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__);
//参数prio在头文件的android_LogPriority枚举中选择

c操作java字符串

c中没有string类型,故须将java字符串转化为c语言的char数组

c操作java数组

1
2
3
int length=(*env)->getArrayLength(env,jarray);
int* array=(*env)->getIntArrayElements(env,jarray,0);
//对该数组进行操作,实际操作的都是传进来的jarray,直接返回即可

4.c代码调用java代码

1.调用自己的方法。env参数指向java虚拟机环境,jobject指向本对象

1
2
3
4
5
//获取类的字节码文件
jclass clazz = (*env)->FindClass(env,"com/example/MainActivity");
//获取方法签名、参数和返回值,可使用javap命令获取方法签名
jmethodID methodId=(*env)->GetMethodID(env,clazz,"helloFromJava","()V");
(*env)->CallVoidMethod(env,jobject,methodId);

2.当在本对象里调用其它类的方法时,需要创建那个类的对象,方法如下

1
jobject obj=(*env)->AllocObject(env,clazz);

3.调用静态方法

1
2
jmethodID methodId=(*env)->GetStaticMethodID(env,clazz,"methodname","()V");
(*env)->CallStaticVoidMethod(clazz,methodId);

补充知识

java方法签名

java和c的比较

java byte short char int float double long
占用字节数 1 2 2 4 4 8 8
c char short long int float double
占用字节数 1 2 4 4 4 8
  • java中int float double short类型可以直接用c中相应类型代替
  • c中的char可用java中的byte代替
  • c中的long可用java中int代替
  • java中的long可用c中的long long代替
  • 指针(表示一个地址),指针变量(变量的值为地址)
  • *在类型后表示该类型的指针变量,int p=&i; 则p=i;表示p指向的地址里存的值,p为int型的指针变量,p的值为i的地址
  • *在变量前表示该地址里存的值,*p = i
  • 动态分配内存 *p=(int*)malloc(sizeof(int));
  • c语言内存:

    • .data 常量池
    • .code 代码段
    • .stack 所有静态分配的内存都放在栈内存中,连续分配
    • 堆内存 动态分配的内存都放在堆内存中,不连续分配
  • 函数指针

    1
    2
    3
    int (*pf)(int x,int y);
    pf = add;
    pf(x,y);


学习中…